/**
 * Funf: Open Sensing Framework
 * Copyright (C) 2010-2011 Nadav Aharony, Wei Pan, Alex Pentland. 
 * Acknowledgments: Alan Gardner
 * Contact: nadav@media.mit.edu
 * 
 * This file is part of Funf.
 * 
 * Funf is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of 
 * the License, or (at your option) any later version. 
 * 
 * Funf is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 * See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public 
 * License along with Funf. If not, see <http://www.gnu.org/licenses/>.
 */
package edu.mit.media.funf.storage;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.http.HttpException;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.NetworkInfo.State;
import android.os.Binder;
import android.os.IBinder;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import edu.mit.media.funf.EqualsUtil;
import edu.mit.media.funf.HashCodeUtil;
import edu.mit.media.funf.Utils;

public abstract class UploadService extends Service {
  public static final String TAG = UploadService.class.getSimpleName();
  
	public static final int
	MAX_REMOTE_ARCHIVE_RETRIES = 5,
	MAX_FILE_RETRIES = 3;
	
	public static final int
		NETWORK_ANY = 0,
		NETWORK_WIFI_ONLY = 1;
	
	public static final String 
  	ARCHIVE_ID = "archive_id",
  	REMOTE_ARCHIVE_ID = "remote_archive_id",
  	NETWORK = "network";

	private ConnectivityManager connectivityManager;
	private Map<String, Integer> fileFailures;
	private Map<String, Integer> remoteArchiveFailures;
	private Set<ArchiveFile> filesToUpload;
	private Thread uploadThread;
	private WakeLock lock;
	
	@Override
	public void onCreate() {
	  super.onCreate();
	  
		Log.i(TAG, "OnCreate()");
		
		connectivityManager =(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
		lock = Utils.getWakeLock(this);
		fileFailures = new HashMap<String, Integer>();
		remoteArchiveFailures = new HashMap<String, Integer>();
		//The next file container is being used ONLY by SINGLE working thread, hence not syncing 
		filesToUpload = new LinkedHashSet<ArchiveFile>();
		
		uploadThread = null;
	}
	
	
	public static final String INNER_TAG = UploadThread.class.getSimpleName();
	private class UploadThread extends Thread{
	  public UploadThread(final Intent intent){
	    super(new Runnable(){
        @Override
        public void run() {
          Log.i(INNER_TAG, "run()");

          Log.i(TAG, "Queuing files...");
          int network = intent.getIntExtra(NETWORK, NETWORK_ANY);
          if (isOnline(network)) {
            String archiveName = intent.getStringExtra(ARCHIVE_ID);
            String remoteArchiveName = intent.getStringExtra(REMOTE_ARCHIVE_ID);
            if (archiveName != null && remoteArchiveName != null) {
              Archive<File> archive = getArchive(archiveName);
              RemoteArchive remoteArchive = getRemoteArchive(remoteArchiveName);
              if (archive != null && remoteArchive != null) {
                for (File file : archive.getAll()) {
                  archive(archive, remoteArchive, file, network);
                }
              }
            }
            
            Log.i(TAG, "Uploading files...");
            while(Thread.currentThread().equals(uploadThread) && !filesToUpload.isEmpty()) {
              Iterator<ArchiveFile> it = filesToUpload.iterator(); 
              ArchiveFile archiveFile = it.next(); 
              it.remove();
              runArchive(archiveFile.archive, archiveFile.remoteArchive, archiveFile.file, archiveFile.network);
            }
          }
        
          uploadThread = null;
          stopSelf(); //stopping the service
        }
	    });
	  }
	}
	
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		Log.i(TAG, "onStartCommand()");
		
    //Do not start upload thread unless the previous one has finished its job
    if (uploadThread == null || !uploadThread.isAlive()) {
      uploadThread = new UploadThread(intent);
      uploadThread.start();
    }
		
		return START_REDELIVER_INTENT;
	}
	
 @Override
  public void onDestroy() {
    Log.i(TAG, "OnDestroy()");
    
    if (uploadThread != null && uploadThread.isAlive()) {
      uploadThread = null;
    }
    
    if (lock.isHeld()) {
      lock.release();
    }
  }

	
	/**
	 * Returns the file archive that the upload service will use.
	 * @param databaseName
	 * @return
	 */
	public Archive<File> getArchive(String name) {
		return DefaultArchive.getArchive(this, name);
	}

	/**
	 * Get the remote archive with the specified name
	 * @param name
	 * @return
	 */
	protected abstract RemoteArchive getRemoteArchive(final String name);
	
	public void archive(Archive<File> archive, RemoteArchive remoteArchive, File file, int network) {
		ArchiveFile archiveFile = new ArchiveFile(archive, remoteArchive, file, network);
		if (!filesToUpload.contains(archiveFile)) {
			Log.i(TAG, "Queuing " + file.getName());
			filesToUpload.add(archiveFile);
		}else
		  throw new IllegalArgumentException("There was an attempt to archive " + file.getAbsolutePath() + " for the second time!");
	}
	
	protected void runArchive(Archive<File> archive, RemoteArchive remoteArchive, File file, int network) {
		Integer numRemoteFailures = remoteArchiveFailures.get(remoteArchive.getId());
		numRemoteFailures = (numRemoteFailures == null) ? 0 : numRemoteFailures;
		if (numRemoteFailures < MAX_REMOTE_ARCHIVE_RETRIES && isOnline(network)) {
			Log.i(TAG, "Archiving..." + file.getName());
			try{
			  remoteArchive.add(file);
			  Log.d(TAG, "Archiving " + file.getName() + " succeeded!");
        archive.remove(file);
			}catch(HttpException e){ //the upload was fine, but the server could not process the data... meaning the data is bogus
			  Log.w(TAG, "Archiving " + file.getName() + " failed with " + e);
			  
        Integer numFileFailures = fileFailures.get(file.getName());
        numFileFailures = (numFileFailures == null) ? 1 : numFileFailures + 1;
        fileFailures.put(file.getName(), numFileFailures);
        // A number of attempts
        if (numFileFailures < MAX_FILE_RETRIES) {
          ArchiveFile archiveFile = new ArchiveFile(archive, remoteArchive, file, network);
          filesToUpload.add(archiveFile);
          Log.i(TAG, "Just re-added " + archiveFile.file.getAbsolutePath() + " back as it failed to upload.");
        } else {
          Log.e(TAG, "Failed to upload '" + file.getAbsolutePath() + "' after " + MAX_FILE_RETRIES + " attempt(s). Deleting the file.");
          archive.remove(file);//ACHTUNG!!! We behave as it has been uploaded successfully. We just delete it at it does not make any sense.
                               //We might consider storing it locally or similar, but it would not make any sense in the case of real users anyway.
        }
			}catch(Exception e) {
				numRemoteFailures += 1;
				Log.w(TAG, "Archiving " + file.getName() + " failed " + numRemoteFailures + " out of " + MAX_REMOTE_ARCHIVE_RETRIES + " allowable times with " + e);
        remoteArchiveFailures.put(remoteArchive.getId(), numRemoteFailures);
			}
		} else {
			Log.i(TAG, "Canceling upload.  Remote archive '" + remoteArchive.getId() + "' is not currently available.");
		}
	}
	
	/**
	 * Convenience class for pairing the database name with the db file
	 */
	protected class ArchiveFile {
		public final Archive<File> archive;
		public final RemoteArchive remoteArchive;
		public final File file;
		public final int network;
		public ArchiveFile(Archive<File> archive, RemoteArchive remoteArchive, File file, int network) {
			this.archive = archive;
			this.remoteArchive = remoteArchive;
			this.file = file;
			this.network = network;
		}
		@Override
		public boolean equals(Object o) {
			return o != null && o instanceof ArchiveFile 
				&& EqualsUtil.areEqual(remoteArchive.getId(), ((ArchiveFile)o).remoteArchive.getId())
				&& EqualsUtil.areEqual(file, ((ArchiveFile)o).file);
		}
		@Override
		public int hashCode() {
			return HashCodeUtil.hash(HashCodeUtil.hash(HashCodeUtil.SEED, file), remoteArchive.getId());
		}
	}
	
	public boolean isOnline(int network) {
	    NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
	    if (network == NETWORK_ANY && netInfo != null && netInfo.isConnectedOrConnecting()) {
	        return true;
	    } else if (network == NETWORK_WIFI_ONLY ) {
		    State wifiInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState();
		    if (State.CONNECTED.equals(wifiInfo) || State.CONNECTING.equals(wifiInfo)) {
		    	return true;
		    }
	    }
	    return false;
	}

	/**
	 * Binder interface to the probe
	 */
	public class LocalBinder extends Binder {
		public UploadService getService() {
            return UploadService.this;
        }
    }
	private final IBinder mBinder = new LocalBinder();
	@Override
	public IBinder onBind(Intent intent) {
		return mBinder;
	}
}
